package tirateima.gui.arquivos;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;

import tirateima.gui.variaveis.VarArray;
import tirateima.gui.variaveis.VarChar;
import tirateima.gui.variaveis.VarGrade;
import tirateima.gui.variaveis.VarInteger;
import tirateima.gui.variaveis.VarReal;
import tirateima.gui.variaveis.VarRecord;
import tirateima.gui.variaveis.Variavel;

@SuppressWarnings("serial")
class CustomRenderer extends DefaultTableCellRenderer 
{
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
    {
        Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        if (row*nColumns + column < highlightPos)
        	c.setBackground(new java.awt.Color(255, 255, 160));
        else if (row*nColumns + column == highlightPos)
        	c.setBackground(new java.awt.Color(255, 255, 0));
        else
        	c.setBackground(new java.awt.Color(255, 255, 255));
        return c;
    }
    
    int nColumns = 1;
    int highlightPos = -1;
    
    public void setNColumns(int n) {
    	nColumns = n;
    }
    
    public void setHLPos (int hlp) {
    	if (hlp < 0)
    		highlightPos = hlp;
    	else
    		highlightPos = hlp/3 + (hlp % 3 > 0 ? 1 : 0);
    }
}

@SuppressWarnings("serial")
public class VarBinFile extends AbstractArquivo {
	
	private JTable pPrincipal;
	private StringBuffer conteudo = null;
	CustomRenderer renderer = null;
	private int posicaoPonteiro = -1;
	private final int nColumns = 18, nRows = 15;
	
	class InternalState {
		JTable pPrincipal = null;
		StringBuffer conteudo = null;
		CustomRenderer renderer = null;
		int posicaoPonteiro = 0;
	}

	public VarBinFile(String nome) {
		super(nome);
		inicializar();
	}
	
	private void inicializar(){
		setLayout(new GridBagLayout());
		GridBagConstraints gbc = new GridBagConstraints();
		gbc.anchor = GridBagConstraints.CENTER;
		gbc.fill = GridBagConstraints.BOTH;
		gbc.weightx = gbc.weighty = 1.0;
		
		add(new JScrollPane(getPPrincipal()), gbc);
	}
	
	private JTable getPPrincipal(){
		if(pPrincipal == null){
			pPrincipal = new JTable(nRows,nColumns);
			pPrincipal.setFont(pPrincipal.getFont().deriveFont(22.0f));
			pPrincipal.setRowHeight(50);
			renderer = new CustomRenderer();
			renderer.setNColumns(nColumns);
			pPrincipal.setDefaultRenderer(Object.class, renderer);
			pPrincipal.setTableHeader(null);
			pPrincipal.setIntercellSpacing(new Dimension(8, 8));
			pPrincipal.setGridColor(new Color(0xdcdcdc));
			pPrincipal.setBackground(new Color(0xdcdcdc));
		}
		return pPrincipal;
	}

	public Object getEstado() {
		InternalState state = new InternalState();
		if (this.conteudo != null) {
			state.conteudo = new StringBuffer(this.conteudo.toString());
		}
		else {
			state.conteudo = null;
		}
		state.posicaoPonteiro = this.posicaoPonteiro;
		state.pPrincipal = this.pPrincipal;
		state.renderer = this.renderer;
		
		return state;
	}

	public void setEstado(Object estado) {
		InternalState state = (InternalState) estado;
		this.conteudo = state.conteudo;
		this.posicaoPonteiro = state.posicaoPonteiro;
		this.renderer = state.renderer;
		
		this.pPrincipal = state.pPrincipal;
		
		/*if (isOpen || firstTime) {
			this.pPrincipal = state.pPrincipal;
			firstTime = false;
		}
		else {
			//Parece redundante, mas o comportamento abaixo é necessário devido à implementação do método getPPrincipal
			this.pPrincipal = null;
			this.pPrincipal = this.getPPrincipal();
		}*/
		
		renderer.setHLPos(posicaoPonteiro);
		
		setTableContents();
		atualizaConteudo();
		pPrincipal.repaint();
	}
	
	private static byte[] readFileAsBuffer(File file) throws java.io.IOException{
	    byte[] buffer = new byte[(int) file.length()];
	    BufferedInputStream f = null;
	    try {
	        f = new BufferedInputStream(new FileInputStream(file.getPath()));
	        f.read(buffer);
	    } 
	    catch (Exception e) {
			e.printStackTrace();
		}
	    finally {
	        if (f != null) try { f.close(); } catch (IOException ignored) { }
	    }
	    return buffer;
	}
	
	private void setTableContents () {
		if (this.conteudo == null)
			return;
		
		for (int i=0; i < this.conteudo.toString().length()/3; i++) {
			String s = "";
			for (int j=0; j < 3; j++) {
				s = s + this.conteudo.toString().charAt(3*i+j);
			}
			pPrincipal.setValueAt(s, i/nColumns, i%nColumns);
		}
		
		for (int i=this.conteudo.toString().length()/3; i < nColumns * nRows; i++) {
			pPrincipal.setValueAt("", i/nColumns, i%nColumns);
		}
	}

	@Override
	public void open(String nome_arq) throws Exception {
		posicaoPonteiro = 0;
		
		file = nome_arq == null ? null : new File(nome_arq);
		titulo = nome + (file == null ? "" : " : '" + file.getName() + "'");
		fileNameAux = new String(file.getName());
		borda.setTitulo(titulo);
		
		if (file != null && file.exists())
		{
			this.conteudo = new StringBuffer(toHex(readFileAsBuffer(file)));
		}
		else
		{
			this.conteudo = new StringBuffer();
		}

		renderer.setHLPos(0);
		
		setTableContents();
		atualizaConteudo();
		pPrincipal.repaint();
		
	}
	
	/* Usado pelo método toHex */
	protected static final byte[] hexchars = {
		'0', '1', '2', '3', '4', '5',
		'6', '7', '8', '9', 'a', 'b',
		'c', 'd', 'e', 'f'
	};


	/* Converte um vetor de bytes pro formato hexadecimal, em uma string */
	private static String toHex(byte buffer[]) {

		StringBuilder s = new StringBuilder(3 * buffer.length);

		for (int i = 0; i < buffer.length; i++) {
			int v = buffer[i] & 0xff;
	
			s.append((char)hexchars[v >> 4]);
			s.append((char)hexchars[v & 0xf]);
			s.append(' ');
		}

		return s.toString();
	}
	
	public void atualizaConteudo()
	{
		
		renderer.setHLPos(posicaoPonteiro);
		
	}

	@Override
	public void reset() throws Exception {
		
	}

	@Override
	public void rewrite() throws Exception {
		
	}

	@Override
	public Object read() throws IOException {
		return null;
	}
	
	/* Remove TODOS os espaços da string de entrada, diferente de String.trim(), que só limpa o início e o final */
	private String removeEspacos (String entrada) {
		String saida = new String();
		
		for (int i=0; i < entrada.length(); i++) {
			if (entrada.charAt(i) != ' ')
				saida = saida + entrada.charAt(i);
		}
		
		return saida;
	}
	
	/* Insere espaços a cada dois caracteres, pra formatar strings em hexa de forma compatível com a string conteudo */
	private String insereEspacos (String entrada) {
		String saida = new String();
		
		for (int i=0; i <= entrada.length(); i++) {
			if ((i > 0) && (i % 2 == 0))
				saida += ' ';
			
			if (i < entrada.length())
				saida += entrada.charAt(i);
		}
		
		return saida;
	}
	
	String endianess (String orig) {
		String ret = new String();
		for (int i=0; i < 8; i++) {
			if (i % 2 == 0) {
				ret = ret.concat(orig.substring(6-i, 6-i+1));
			}
			else {
				ret = ret.concat(orig.substring(8-i, 8-i+1));
			}
		}
		return ret;
	}
	
/*	 Retorna todos os elementos de um hashmap concatenados em uma String
	private String getElementosHashMap(HashMap<String, Object> map) {
		String elementos = "";
		for (String str : map.keySet()) {
			Object element = map.get(str);
			if (element instanceof HashMap<?, ?>) {
				elementos = elementos.concat(getElementosHashMap((HashMap<String, Object>)element));
			}
			else {
				elementos = elementos.concat(element.toString());
			}
		}
		return elementos;
	}*/
	
	public Object fread(Variavel type) throws IOException {
		Object ret = null;
		//for (int i=0; i < nmemb; i++) {
			if (type instanceof VarInteger || (type instanceof VarArray && type.typeName().equals("int"))) {
				int offset = conteudo.length() >= posicaoPonteiro + 12 ? 0 : posicaoPonteiro + 12 - conteudo.length();
				String tempStr = conteudo.substring(posicaoPonteiro - offset, posicaoPonteiro + 12 - offset);
				tempStr = removeEspacos(tempStr);
				tempStr = endianess(tempStr);
				int temp = Integer.parseInt(tempStr, 16);
				ret = new Integer(temp);
				posicaoPonteiro += 12 - offset;
			}
			else if (type instanceof VarReal || (type instanceof VarArray && type.typeName().equals("real"))) {
				int offset = conteudo.length() >= posicaoPonteiro + 12 ? 0 : posicaoPonteiro + 12 - conteudo.length();
				String tempStr = conteudo.substring(posicaoPonteiro - offset, posicaoPonteiro + 12 - offset);
				tempStr = removeEspacos(tempStr);
				tempStr = endianess(tempStr);
				Float temp = null;
				try {
					Long l = Long.parseLong(tempStr, 16);
					temp = Float.intBitsToFloat(l.intValue());
					ret = new Float(temp);
				}
				catch (Exception e) {
					System.out.println("Tentativa de fazer parse de float a partir de string com formato inválido");
					System.out.println(e.toString());
				}
				posicaoPonteiro += 12 - offset;
			}
			else if (type instanceof VarChar || (type instanceof VarArray && type.typeName().equals("char"))) {
				int offset = conteudo.length() >= posicaoPonteiro + 3 ? 0 : posicaoPonteiro + 3 - conteudo.length();
				String tempStr = conteudo.substring(posicaoPonteiro - offset, posicaoPonteiro + 3 - offset);
				tempStr = removeEspacos(tempStr);
				int j = 0;
				try {
					j = Integer.parseInt(tempStr, 16);
				}
				catch (Exception e) {
					System.out.println(e.toString());
				}
				ret = new Character ((char) j);
				//posicaoPonteiro += 3;
				posicaoPonteiro += 6;
			}
			else if (type instanceof VarRecord || type instanceof VarArray) {
				try {
					if (type instanceof VarRecord || ((VarGrade) type).getCopiaTipo() instanceof VarRecord) {
						int indiceDelimitador = 0;
						String tempStr = conteudo.substring(posicaoPonteiro, conteudo.indexOf("#", posicaoPonteiro));
						tempStr = tempStr.replaceAll(" ", "");
						byte[] array = hexStringToByteArray(tempStr);
						tempStr = new String(array);
						System.out.println(tempStr);
						HashMap<String, Object> newMap = convertToStringToHashMap(tempStr);
						ret = newMap;

						tempStr = conteudo.substring(posicaoPonteiro);
						posicaoPonteiro = conteudo.substring(0, posicaoPonteiro).length() + tempStr.indexOf("#") + 1; 
					}
				} catch (Exception e) {
				}
			}
			else {
				System.out.println("Tentando ler para tipo de variável desconhecido");
				System.out.println(type.typeName());
			}
		//}
		
		atualizaConteudo();
		
		return ret;
	}
	
	public byte[] hexStringToByteArray(String s) {
	    int len = s.length();
	    byte[] data = new byte[len / 2];
	    for (int i = 0; i < len; i += 2) {
	        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
	                             + Character.digit(s.charAt(i+1), 16));
	    }
	    return data;
	}
	
	public void fwrite(int nmemb, Object value) throws IOException {
		String toWrite = null;
		for (int i=0; i < nmemb; i++) {
			//TODO Isso vai dar pau no caso de nmemb > 1, verificar implementação com arrays
			if (value instanceof Integer) {
				toWrite = Integer.toHexString(((Integer) value).intValue());
				if (toWrite.length() < 8) {
					String newStr = new String();
					for (int j=0; j < 8 - toWrite.length(); j++)
						newStr += '0';
					toWrite = newStr.concat(toWrite);
				}
				toWrite = endianess(toWrite);
			}
			else if (value instanceof Double) {
				//TODO Note que há um bug na implementação de VarFloat que faz com que na verdade a função abaixo retorne a conversão pra int, e não pra float. Olhe a documentação do método floatToIntBits antes de mexer aqui!
				toWrite = Integer.toString(Float.floatToIntBits(((Double) value).floatValue()), 16);
				toWrite = endianess(toWrite);
			}
			else if (value instanceof Character) {
				toWrite = Integer.toString(((Character) value).charValue(), 16);
			}
			else if (value.getClass().isArray() && ((Object[]) value)[i] instanceof HashMap<?, ?>) {
				HashMap<String, Object> map = (HashMap<String, Object>) ((Object[]) value)[i];
				String str = map.toString();
				str = new BigInteger(str.getBytes()).toString(16);
				
				
				if (toWrite == null)
					toWrite = "";
				toWrite = toWrite + str + "#";

			}
			else {
				System.out.println("Tentando ler para tipo de variável desconhecido: " + value.getClass().getName());
				toWrite = "Variável não identificada";
			}
		}
		
		toWrite = insereEspacos(toWrite);
		
		for (int i=0; i < toWrite.length(); i++) {
			if (posicaoPonteiro + i < conteudo.length()) {
				conteudo.setCharAt(posicaoPonteiro + i, toWrite.charAt(i));
			}
			else {
				conteudo = conteudo.append(toWrite.charAt(i));
			}
		}
		
		posicaoPonteiro += toWrite.length();
		
		setTableContents();
		atualizaConteudo();
		pPrincipal.repaint();
		
		
	}

/*	private String serializedMap(HashMap<String, Object> map) {
		try {
			ByteArrayOutputStream out = new ByteArrayOutputStream();
	        ObjectOutputStream objOut;
				objOut = new ObjectOutputStream(out);
	        objOut.writeObject(map);
	        objOut.close();
	        //ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()));
	        //HashMap<String, Object> newMap = (HashMap<String, Object>) objIn.readObject();
	        byte[] a = out.toByteArray();
	        String oi = new String(a);
	        return out.toByteArray().toString();
		} catch (IOException e) {
			e.printStackTrace();
			return "";
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
			return "";
		}
	}*/
	
	private HashMap convertToStringToHashMap(String text){
		HashMap data = new HashMap<String, Object>();
		
		Pattern subHash = Pattern.compile("([ {]([^\\=]*)\\=\\{([^\\}]*)\\})");
		Matcher matcher = subHash.matcher(text);
		while (matcher.find()) {
			String sub = matcher.group();
			sub = sub.substring(1); //retira o espaco
			String key = sub.split("\\=")[0];
			String value = sub.substring(key.length()+1);
			data.put(key, convertToStringToHashMap(value));
		}
		
		Pattern subHash2 = Pattern.compile("(((, )|(\\{))([^\\=]*)\\=\\{([^\\}]*)\\})"); //pega a virgula tambem
		matcher = subHash2.matcher(text);
		while (matcher.find()) { //deixa a string sem os subhashs
			text = text.replace(matcher.group(), "");
		}
		
		Pattern p = Pattern.compile("[\\{\\}\\=\\, ]++");
		String[] split = p.split(text);
		for ( int i=1; i < split.length; i+=2 ){
			data.put(split[i], split[i+1] );
		}
		return data;
	}

	@Override
	public boolean eof() {
		return posicaoPonteiro >= conteudo.length();
	}

	@Override
	public void close() throws IOException {
		conteudo = new StringBuffer();
		titulo = "";
		borda.setTitulo(nome);
		posicaoPonteiro = -1;
		for (int i=0; i < nRows * nColumns; i++) {
			pPrincipal.setValueAt("", i/nColumns, i%nColumns);
		}
		atualizaConteudo();
	}

	@Override
	public Color getCorTitulo() {
		return null;
	}

	public void atualizaPosicaoPonteiro(int posicao, String seekset) {
		int prelativa = 0;
		
		
		if(seekset.equals("[SEEK_SET]")){
			prelativa=0;
		}
		else if (seekset.equals("[SEEK_CUR]")){
			prelativa  = this.posicaoPonteiro;
		}
		else if (seekset.equals("[SEEK_END]")){
			prelativa = this.conteudo.length();
		}
		
		posicaoPonteiro = prelativa + posicao;
		
		if (posicaoPonteiro < 0)
			posicaoPonteiro = 0;
		else if (posicaoPonteiro > this.conteudo.length())
			posicaoPonteiro = this.conteudo.length();
		
		System.out.println(posicaoPonteiro);
		
		atualizaConteudo();
		pPrincipal.repaint();		
	}

}
